package pl.edu.pw.stud.ltrzaska.classloader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Vector;

import org.apache.log4j.Logger;

import pl.edu.pw.stud.ltrzaska.exception.ClassloaderException;

/**
 * Klasa sluzaca do dynamicznego ladowania klasy (w runtime) na podstawie jej
 * pelnej nazwy (fully-qualified java class name) oraz bytecode (tresci pliku
 * .class).
 */
public class BinaryClassLoader extends ClassLoader {
	private final Logger LOGGER = Logger.getLogger(ClassLoader.class);
	private Vector<String> loadableClasses = new Vector<String>();
	private byte[] classBytes;

	/**
	 * Metoda-fabryka do wygodnego tworzenia obiektu ClassLoader-a z mozliwoscia
	 * ladowania klas potomnych.
	 * 
	 * @return obiekt umozliwiajacy dynamiczne ladowanie klas potomnych
	 */
	public static BinaryClassLoader create() {
		ClassLoader parentClassLoader = BinaryClassLoader.class.getClassLoader();
		BinaryClassLoader classLoader = new BinaryClassLoader(parentClassLoader);
		return classLoader;
	}

	/**
	 * Konstruktor bierze jako argument klase rodzica, aby moc ladowac klasy,
	 * ktore same po kims dziedzicza.
	 * 
	 * @param parent
	 *            Obiekt ladujacy klase bazowa.
	 */
	private BinaryClassLoader(ClassLoader parent) {
		super(parent);
	}

	/**
	 * Laduje bajty klasy z url-a do bufora.
	 * 
	 * @param url
	 *            Adres ladowanej klasy.
	 * @return 
	 * @throws PoltaxIntegrationException
	 */
	public byte[] loadFileToClassBytes(String url) throws ClassloaderException {
		URL myUrl;
		byte[] buf = null;
		
		try {
			myUrl = new URL(url);

			URLConnection connection = myUrl.openConnection();
			InputStream input = connection.getInputStream();
			ByteArrayOutputStream buffer = new ByteArrayOutputStream();
			int data = 0;

			while ((data=input.read()) != -1) {
				buffer.write(data);
			}
			input.close();
			buf = buffer.toByteArray();
			this.setClassBytes(buf);
			return buf;
		} catch (MalformedURLException e) {
			throw new ClassloaderException("Cannot load class file from URL.");
		} catch (IOException e) {
			throw new ClassloaderException("IOException occured while loading class bytecode from file.");
		}
	}

	/**
	 * addLoadableClass Dodaj klase do zbioru "mozliwych do zaladowania". Zbior
	 * ten sluzy do rozroznienia pomiedzy klasa bazowa, a pochadna, ktorej.
	 * 
	 * @param className
	 *            nazwa klasy do zarejestrowania.
	 */
	public void addLoadableClass(String className) {
		loadableClasses.add(className);
	}

	/**
	 * Laduje klase o nazwie name z bufora bajtow buff.
	 * 
	 * @param name
	 *            nazwa klasy, ktora bedzie ladowana.
	 * @param bytes
	 *            bufor bajtow zawierajacy implementacje klasy.
	 * @return {@link java.lang.Class} dynamicznie zaladowany obiekt
	 *         <code>Class</code>
	 * @throws PoltaxLdapException
	 * @throws PoltaxIntegrationException
	 */
	public Class<?> loadClass(String name, byte[] bytes) throws ClassloaderException {
		this.addLoadableClass(name);
		Class<?> co = null;
		this.setClassBytes(bytes);
		try {
			co = this.loadClass(name);
		} catch (ClassNotFoundException e) {
			throw new ClassloaderException("Nazwa klasy do zaladowania nie zostala znaleziona.", e.fillInStackTrace());
		}
		return co;
	}

	/**
	 * loadClass Laduj klase o nazwie name z lokalizacji na url.
	 * 
	 * @param name
	 *            nazwa klasy do zaladowania.
	 * @param baseUrl
	 *            url bazowy (bez nazwy pliku) pod ktorym bedzie poszukiwany
	 *            plik *.class.
	 * @return {@link java.lang.Class} dynamicznie zaladowany obiekt
	 *         <code>Class</code>
	 * @throws PoltaxIntegrationException
	 */
	public Class<?> loadClass(String name, String baseUrl) throws ClassloaderException {
		String url = baseUrl + "/" + name + ".class";
		this.addLoadableClass(name);

		this.loadFileToClassBytes(url);
		Class<?> co = null;
		try {
			co = this.loadClass(name);
		} catch (ClassNotFoundException e) {
			throw new ClassloaderException("Nazwa klasy do zaladowania nie zostala znaleziona.");
		}
		return co;
	}

	/**
	 * Laduje klasy: jezeli klasa zostala dodana do loadableClasses nastepuje
	 * proba ladowania z bajt-kodu. W przeciwnym wypadku nastepuje klasyczne
	 * wczytanie <code>super.loadClass(name)</code>.
	 * 
	 * @param name
	 *            nazwa klasy do zaladowania.
	 * @return {@link java.lang.Class} dynamicznie zaladowany obiekt
	 *         <code>Class</code>
	 * @throws ClassNotFoundException
	 */
	public Class<?> loadClass(String name) throws ClassNotFoundException {
		if (-1 == loadableClasses.indexOf(name)) {
			return super.loadClass(name);
		}
		return defineClass(name, this.getClassData(), 0, this.getClassData().length);
	}

	public void setClassBytes(byte[] bytes) {
		this.classBytes = bytes;
	}

	public byte[] getClassData() {
		return classBytes;
	}
}
